/*
 *    Copyright 2009-2023 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.builder.xml;

import java.util.List;
import java.util.Locale;

import org.apache.ibatis.builder.BaseBuilder;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.builder.annotation.MapperAnnotationBuilder;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;

/**
 * @author Clinton Begin
 */
public class XMLStatementBuilder extends BaseBuilder {

  private final MapperBuilderAssistant builderAssistant;
  private final XNode context;
  private final String requiredDatabaseId;

  public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) {
    this(configuration, builderAssistant, context, null);
  }

  public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context,
      String databaseId) {
    super(configuration);
    this.builderAssistant = builderAssistant;
    this.context = context;
    this.requiredDatabaseId = databaseId;
  }

  /**
   * 解析insert/update/select/del中的标签
   */
  public void parseStatementNode() {
    /**
     * 在命名空间中唯一的标识符,可以被用来引用这条语句
     *
     * 可以理解成一个id就是mapper中的一个方法名
     * <select id="selectUser" databaseId="mysql" resultType="User">
     *     ELECT * FROM users WHERE id = #{id}
     * </select>
     *
     **/
    String id = context.getStringAttribute("id");
    /**
     * 数据库厂商标识
     * 例如:
     * <configuration>
     *     <!-- 其他配置 -->
     *
     *     <!-- 配置 databaseIdProvider -->
     *     <databaseIdProvider type="DB_VENDOR">
     *         <property name="MySQL" value="mysql"/>
     *         <property name="Oracle" value="oracle"/>
     *         <!-- 其他数据库的配置 -->
     *     </databaseIdProvider>
     *
     *     <!-- 映射器 -->
     *     <mappers>
     *         <mapper resource="com/example/mapper/UserMapper.xml"/>
     *     </mappers>
     * </configuration>
     *
     *
     * <mapper namespace="com.example.mapper.UserMapper">
     *     <!-- MySQL 特定的 SQL 语句 -->
     *     <select id="selectUser" databaseId="mysql" resultType="User">
     *         SELECT * FROM users WHERE id = #{id}
     *     </select>
     *
     *     <!-- Oracle 特定的 SQL 语句 -->
     *     <select id="selectUser" databaseId="oracle" resultType="User">
     *         SELECT * FROM users WHERE id = #{id}
     *     </select>
     * </mapper>
     **/
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    // 解析SQL命令类型是什么？确定操作是CRUD中的哪一种
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    /**
     * flushCache和useCache都和二级缓存有关
     * 将其设置为true后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false
     *
     * flushCache：如果当前不是查询语句（即 INSERT, UPDATE, DELETE），默认值为 true。这意味着在执行非查询语句时，默认会清空缓存。
     */
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    /**
     * 将其设置为 true 后，将会导致本条语句的结果被二级缓存缓存起来，默认值：对 select 元素为 true
     *
     * useCache：如果当前是查询语句，默认值为 true。这意味着在执行查询语句时，默认会使用缓存。
     */
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    /**
     * 这行代码从上下文中获取 resultOrdered 属性的值。
     * 如果这个属性没有在 XML 配置中明确指定，其默认值为 false。
     * resultOrdered 属性通常用于指示查询结果是否需要按特定顺序返回。
     */
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    // <include>标签解析
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    /**
     * 会传入这条语句的参数类的完全限定名或别名
     *
     * 例如：
     * <insert id="insertUser" parameterType="org.apache.ibatis.entity.User">
     *     INSERT INTO users (username, password, email, created_at, updated_at)
     *     VALUES (#{username}, #{password}, #{email}, NOW(), NOW())
     * </insert>
     *
     * {@link org.apache.ibatis.entity.User}
     *
     * parameterType参数内容
     */
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    /**
     * context.getStringAttribute("lang") 用于获取 XML 配置文件中 <select>、<insert>、<update> 或 <delete> 元素的 lang 属性。
     * 这个 lang 属性指定了使用的语言驱动（Language Driver），它允许 MyBatis 使用不同的语言或方言来解析和执行 SQL 语句。
     *
     * <mapper namespace="com.example.mapper.UserMapper">
     *     <!-- 使用特定的语言驱动 -->
     *     <select id="selectUser" lang="mybatis" resultType="com.example.entity.User">
     *         SELECT * FROM users WHERE id = #{id}
     *     </select>
     * </mapper>
     */
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    // 解析<selectKey>标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    /**
     * keyStatementId 字段通常用于关联 <selectKey> 元素和 <insert> 元素。
     * <selectKey> 元素用于在执行插入操作之前或之后生成主键值，而 keyStatementId 字段则指向 <selectKey> 元素的 id 属性，
     * 这样 MyBatis 就知道在执行插入操作时应该使用哪个 <selectKey> 元素来生成主键值。
     *
     * 例如：
     * <mapper namespace="com.example.mapper.UserMapper">
     *     <!-- 主键生成的 SQL 语句 -->
     *     <selectKey keyProperty="id" resultType="Long" order="BEFORE">
     *         SELECT LAST_INSERT_ID()
     *     </selectKey>
     *
     *     <!-- 插入用户的 SQL 语句 -->
     *     <insert id="insertUser" keyColumn="id" keyStatementId="selectKey">
     *         INSERT INTO users (username, password, email) VALUES (#{username}, #{password}, #{email})
     *     </insert>
     * </mapper>
     **/
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
              ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    // 创建SqlSource，解析SQL，封装SQL语句（未参数绑定）和入参信息
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    // 设置默认StatementType为Prepared，该参数指定了后面的JDBC处理时，采用哪种Statement
    StatementType statementType = StatementType
        .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    /**
     * 这条语句中返回的期望类型的类的完全限定名或别名
     */
    String resultType = context.getStringAttribute("resultType");
    // 别名处理，获取返回值对应的Java类型
    Class<?> resultTypeClass = resolveClass(resultType);
    /**
     * 外部resultMap的命名引用
     */
    String resultMap = context.getStringAttribute("resultMap");
    if (resultTypeClass == null && resultMap == null) {
      resultTypeClass = MapperAnnotationBuilder.getMethodReturnType(builderAssistant.getCurrentNamespace(), id);
    }
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");
    boolean dirtySelect = context.getBooleanAttribute("affectData", Boolean.FALSE);

    // => 通过构建者助手，创建MappedStatement对象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
        parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
  }

  private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    if (configuration.getDatabaseId() != null) {
      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    removeSelectKeyNodes(selectKeyNodes);
  }

  private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass,
      LanguageDriver langDriver, String skRequiredDatabaseId) {
    for (XNode nodeToHandle : list) {
      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      String databaseId = nodeToHandle.getStringAttribute("databaseId");
      if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
        parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
      }
    }
  }

  private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver,
      String databaseId) {
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    StatementType statementType = StatementType
        .valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    // defaults
    boolean useCache = false;
    boolean resultOrdered = false;
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;

    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
        parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null, false);

    id = builderAssistant.applyCurrentNamespace(id, false);

    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
  }

  private void removeSelectKeyNodes(List<XNode> selectKeyNodes) {
    for (XNode nodeToHandle : selectKeyNodes) {
      nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
    }
  }

  private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    if (requiredDatabaseId != null) {
      return requiredDatabaseId.equals(databaseId);
    }
    if (databaseId != null) {
      return false;
    }
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (!this.configuration.hasStatement(id, false)) {
      return true;
    }
    // skip this statement if there is a previous one with a not null databaseId
    MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2
    return previous.getDatabaseId() == null;
  }

  private LanguageDriver getLanguageDriver(String lang) {
    Class<? extends LanguageDriver> langClass = null;
    if (lang != null) {
      langClass = resolveClass(lang);
    }
    return configuration.getLanguageDriver(langClass);
  }

}
